/*  
 *  Copyright 2006 Paul Jack.
 *
 *  This file is part of the Dex compiler suite. 
 *  
 *  Dex is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the
 *  Free Software Foundation; either version 2 of the License, or (at your
 *  option) any later version.
 *  
 *  Dex is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 *  Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */
package dex.misc;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;


/**
 * Reflection utilities.  The methods in this class require that
 * the security manager allow Mirror access to private fields.
 */
public class Mirror {
	
	
	/**
	 * Static utility library.
	 */
	private Mirror() {
	}

	
	/**
	 * Returns true if the two object have equal instance fields.
	 * The two objects must be of exactly the same type.
	 * 
	 * @param o1  the first object to test
	 * @param o2  the second object to test
	 * @return  true if the two objects have the same fields
	 */
	public static boolean equals(Object o1, Object o2) {
		if (o1 == o2) {
			return true;
		}
		
		if (o1 == null) {
			return false;
		}
		
		if (o2 == null) {
			return false;
		}
		
		if (!o1.getClass().equals(o2.getClass())) {
			return false;
		}
		
		Set<Field> fields = getInstanceFields(o1.getClass());
		for (Field f: fields) {
			if (!equals(f, o1, o2)) {
				return false;
			}
		}
		
		return true;
	}

	
	/**
	 * Determines if the two objects have the same value in the given field.
	 * 
	 * @param f    the field to check
	 * @param o1   the first object
	 * @param o2   the second object
	 * @return  true if the objects have the same value for that field
	 */
	private static boolean equals(Field f, Object o1, Object o2) {
		try {
			f.setAccessible(true);
			Object value1 = f.get(o1);
			Object value2 = f.get(o2);
			if ((value1 == null) && (value2 == null)) {
				return true;
			}
			if ((value1 == null) || (value2 == null)) {
				return false;
			}
			if (value1.getClass().isArray() || value2.getClass().isArray()) {
				return arrayEquals(value1, value2);
			}
			return value1.equals(value2);
		} catch (IllegalAccessException e) {
			throw new IllegalStateException();
		}
	}
	
	
	/**
	 * Returns true if the two arrays are equal.
	 * 
	 * @param a1  the first array to check  
	 * @param a2  the second array to check 
	 * @return  true if those two arrays are equal
	 */
	private static boolean arrayEquals(Object a1, Object a2) {
		Class e1 = a1.getClass().getComponentType();
		Class e2 = a2.getClass().getComponentType();
		if (e1.isPrimitive() && e2.isPrimitive()) {
			return primitiveArrayEquals(a1, a2);
		}
		
		if (e1.isPrimitive() || e2.isPrimitive()) {
			return false;
		}
		
		return Arrays.deepEquals((Object[])a1, (Object[])a2);
	}
	
	
	/**
	 * Returns true if two primitive arrays are equal.
	 * 
	 * @param a1  the first primtive array to check
	 * @param a2  the second primitive array to check
	 * @return   true if those two primitive arrays are equal
	 */
	private static boolean primitiveArrayEquals(Object a1, Object a2) {
		Class e1 = a1.getClass().getComponentType();
		Class e2 = a2.getClass().getComponentType();
		if (!(e1.equals(e2))) {
			return false;
		}
		
		if (e1.equals(Boolean.TYPE)) {
			return Arrays.equals((boolean[])a1, (boolean[])a2);
		} else if (e1.equals(Byte.TYPE)) {
			return Arrays.equals((byte[]) a1, (byte[]) a2);
		} else if (e1.equals(Character.TYPE)) {
			return Arrays.equals((char[]) a1, (char[]) a2);			
		} else if (e1.equals(Double.TYPE)) {
			return Arrays.equals((double[]) a1, (double[]) a2);
		} else if (e1.equals(Integer.TYPE)) {
			return Arrays.equals((int[]) a1, (int[]) a2);
		} else if (e1.equals(Long.TYPE)) {
			return Arrays.equals((long[]) a1, (long[]) a2);
		} else if (e1.equals(Short.TYPE)) {
			return Arrays.equals((short[]) a1, (short[]) a2);
		} else {
			throw new IllegalStateException();
		}
	}

	
	/**
	 * Returns a hash code for the given object.  The hashcode is formed
	 * by hashing each instance field of the object.
	 * 
	 * @param object  the object to hash
	 * @return   a hash code for that object
	 */
	public static int hashCode(Object object) {
		if (object == null) {
			return 0;
		}
		
		// Algorithm courtesy of Joshua Block, _Effective Java_
		int result = 17;
		Set<Field> fields = getInstanceFields(object.getClass());
		for (Field f: fields) {
			result = 37 * result + hashCode(f, object);
		}
		
		return result;
	}

	
	/**
	 * Returns a hash code for the given field of the given object.
	 * 
	 * @param f  the field whose value to hash
	 * @param o  the object who owns the field
	 * @return  a hash code for the value of that field for that object
	 */
	private static int hashCode(Field f, Object o) {
		try {
			f.setAccessible(true);
			Object value = f.get(o);
			if (value == null) {
				return 0;
			}
			if (value.getClass().isArray()) {
				return arrayHashCode(value);
			}
			return value.hashCode();
		} catch (IllegalAccessException e) {
			throw new IllegalStateException();
		}
	}
	
	
	/**
	 * Returns a hash code for the given array.
	 *
	 * @param value  the array whose hash code to return
	 * @return  a hash code for that array
	 */
	private static int arrayHashCode(Object value) {
		if (value instanceof boolean[]) {
			return Arrays.hashCode((boolean[])value);
		}		
		if (value instanceof byte[]) {
			return Arrays.hashCode((byte[])value);			
		}
		if (value instanceof char[]) {
			return Arrays.hashCode((char[])value);			
		}
		if (value instanceof double[]) {
			return Arrays.hashCode((double[])value);			
		}
		if (value instanceof float[]) {
			return Arrays.hashCode((float[])value);			
		}
		if (value instanceof int[]) {
			return Arrays.hashCode((int[])value);			
		}
		if (value instanceof long[]) {
			return Arrays.hashCode((long[])value);			
		}
		if (value instanceof short[]) {
			return Arrays.hashCode((short[])value);			
		}
		
		return Arrays.deepHashCode((Object[])value);
	}

	
	/**
	 * Returns all of the instance fields for the given class.
	 * The returned set will not contain static fields, nor
	 * any fields defined by java.lang.Object.  Otherwise
	 * the returned set contains all of the instance fields
	 * defined by the given class and its superclasses.
	 * 
	 * <p>For this method to work, the security manager must allow
	 * Mirror to access private fields.
	 * 
	 * @param start  the class whose instance fields to return
	 * @return  the set of fields for that class
	 */
	public static Set<Field> getInstanceFields(Class start) {
		Set<Field> result = new HashSet<Field>();
		for (Class c = start; c != Object.class; c = c.getSuperclass()) {
			for (Field f: c.getDeclaredFields()) {
				if (!Modifier.isStatic(f.getModifiers())) {
					result.add(f);
				}
			}
		}
		return result;
	}
	
}
